R Shiny is a fantastic tool for creating beautiful, interactive web apps. One of the most powerful features of Shiny is how easy it is to create functional apps, even if you’re new to the package. On the flip side, styling can be tricky and as a developer, you don’t just want to produce apps that work, but also stunning UIs that enhance the user experience.
There are a range of approaches to styling Shiny apps so when you’re early in your Shiny journey, it can get a bit overwhelming trying to choose the best approach for your app (and that’s without even thinking about CSS!). The syntax for each approach varies, the documentation isn’t necessarily easy to follow, you don’t really know what the result might look like and it can be tough finding examples of code to help your understanding. And then on top of all those concerns, it can also be intimidating to experiment with another new tool as you find your feet with Shiny because you don’t want to break anything!
As with anything, the best way to learn is by experimenting but this guide and its associated repo are here to help you understand some of the different approaches available so you can create beautiful, functional apps sooner rather than later.
We’ll explore three popular approaches to styling Shiny apps:
bslibshinydashboardshiny.semanticMake sure you have these packages installed to follow along and run the apps in the repo. The apps for each framework live in folders named after the framework. It may make it easier to understand what is happening by comparing scripts side-by-side and even doing your own little experiments on the code.
But before we look at how these packages work, we need to build a template app in base Shiny, which you can find the script for in the base Shiny folder. We’ll use this template to then create new apps with the other frameworks.
For the app, we use the Tidy Tuesday dataset on Project FeederWatch from Week 2 of 2023. In short, the data describe the distribution and abundance of bird species across North America over winter 2020/2021 so with this dataset, we can create a simple dashboard that visualises where a selected species has been observed in North America.
In the app script between lines 13 (with the heading “load data”) and 90 (with the heading “function to plot species’ location”), we’re just doing a bit of prep work to clean the data and create a function to produce the map to visualise the distribution and abundance of a given species. (It’s fairly straightforward and the comments should help guide you to understanding what’s happening.)
For the purposes of exploring the styling capabilities available in Shiny, we add a little bit of complexity to our app by having three tabs: one to perform the app’s main function (the visualisation), one to serve as an about section and one to simply display all the data in a table.
In the first tab, we want to give the user the ability to select a
species from the data and we want a plot to visualise the data requested
so we need an input for users to select a species of interest and a plot
output for the map produced by plot_species().
In the about tab, we simply have some text describing Project FeederWatch making use of HTML tags. And, for our data tab, we have a data table displaying all the cleaned data.
A brief overview of what’s happening in the server:
There’s nothing super fancy happening here, we’re mainly using
eventReactive() with a button in the UI triggering when we
update the app so our computed outputs don’t constantly update as the
user plays around with the selectInput() element.
The server remains the same for all the approaches we explore here so from here on, our focus will solely be on the UI.
With the UI and server sorted, let’s see how the app looks (you can also run the app.R script in the base Shiny folder to see how it works). Using base Shiny, we have a pretty basic app that gets the job done and while it’s not ugly, it could certainly look more enticing.
There are two sides to styling our apps, one is the layout (think the overall structure) and the other is theming (think colour schemes and finer details) I generally think it is easier to start the styling process by focussing on the layout before jumping into theming.
In base Shiny, we made an app with three tabs with one of those tabs
having a sidebar layout. We’ll look at how to replicate this in
bslib, shinydashboard and
shiny.semantic. Although we aren’t directly addressing
theming here, you’ll see how each framework has its own aesthetic giving
the app a different feel.
bslibAs we’ll see with the other frameworks, the structure of a
bslib app is slightly different to that of a base Shiny
app. It takes a bit of work to translate the app from one framework to
another but, by and large, there are equivalent functions so the process
just requires a bit of attention to detail.
The bslib app itself looks a lot like the base Shiny app
but more nicely organised, particularly with respect to the header and
the spacing between elements.
Let’s have a look at how the code changes.
A nice feature of bslib is the value box, which can be
an eye-catching way of communicating a result. In the Project
FeederWatch tab, we’ve added a value box that sits in the sidebar and
lines 200-215 in the server to give it its reactive content.
bslib gives us access to Bootstrap’s cards, which
essentially act as containers that we can use to better organise our
apps. While our app doesn’t make use of all the nifty features available
to us with cards, we do use the full_screen option. This is
particularly useful in this case where we have a map that would benefit
from a bigger canvas to fully showcase the data.
You may have noticed fillable = F wrapped inside
page_navbar(). This argument tells Shiny that we don’t want
our outputs to change in size as the user resizes the window. By doing
so, we prevent our outputs from being cut off or shrunk.
shinydashboardshinydashboard is a fair bit different to base Shiny. As
you’ll see below, if you want a sidebar in your app, it’ll be there on
every tab and because of this feature, having your user inputs in the
sidebar only makes sense if they are applicable to all tabs. Due to this
constraint, the sidebar is better suited to arranging tabs in our
case.
Although the app we get looks more complete than the base Shiny one, it’s boxy and not particularly pretty…
dashboardPage()To create our dashboard in shinydashboard, we use
dashboardPage() in a straightforward swap for
fluidPage(). After dashboardPage(), there are
three important functions for defining the layout under the
shinydashboard framework:
dashboardHeader()dashboardSidebar()dashboardBody()dashboardHeader() serves a similar function to
titlePanel(); we use it to define the title of the app. We
also have the option to increase the width of the title’s section in the
header with the titleWidth parameter. This will be
pertinent if you have a long title.
dashboardBody() is where the main contents of the app
lie. To create tabs, we use tabItems() and
tabItem(). tabItem() wraps the code for each
tab while tabItems() wraps around all the tabs.
tabItem() works more or less like
tabPanel() so we just wrap the contents of our tab in the
function. The one important thing to remember is that the
tabName argument must match one of the tabName
arguments defined in menuItem().
In the case of our app, as it wouldn’t make sense to ask the user for
inputs in the sidebar, the input element and the button have been moved
into the body using fluidRow() to define where we would
like to place them.
Like bslib, shinydashboard has a value box
feature. Unlike bslib though, shinydashboard
treats the whole value box as a dynamic element so we render the value
box in the server rather than just the specific parts we want to update
as the user interacts with our app (see lines 200-215). And, we have an
output function specific to the value box in
valueBoxOutput().
Similarly, we have boxes, which are equivalent to
bslib’s cards and can be used as containers. Boxes can be
useful for ensuring there is some padding between elements but they
don’t always do what you want. For example, the height of the box cannot
be changed to display more of the data table output in the FeederWatch
tab.
shiny.semanticThe final approach we’ll explore works under a very different
framework to bslib and shinydashboard. As its
name suggests, shiny.semantic uses Semantic-UI (or rather
Fomantic-UI) instead of Bootstrap. There’s not really too much to worry
about with respect to what that means for how we create our UI in R; the
code just follows a different style.
Under the Semantic framework, we get a very clean looking UI with quite a modern feel but it’s little bare looking.
fluidPage() and ElementsAs with the other frameworks, shiny.semantic has its own
version of base Shiny’s fluidPage():
semanticPage(). And, as with base Shiny, we use
titlePanel() to give us a title panel.
One of the great features of shiny.semantic is that the
names of the functions for elements are the same as base Shiny’s so if
you want to see what your app looks like with a Semantic-UI engine, you
don’t need to change every single element. They are simply rendered as
Semantic style elements. One distinction to note is that under
shiny.semantic, selectInput() does not have a
selectize parameter so user may have to scroll through all
options in our app to find a species of interest.
Creating tabs in shiny.semantic can get a little messy
compared to the other approaches. We use the function
tabset() to create tabs. It takes as its main argument
tabs, which must be given as a list wrapping a set of
lists. Each list in this set defines the contents of a tab. This is
illustrated in line 99 where we have tabs = list( followed
in the next line by list(menu = "Project FeederWatch",. The
lists used to define the contents of a tab (e.g. the list spanning lines
100-131) have three arguments:
menu - gives the tab its name (this is the equivalent
to titlePanel()’s title parameter)content - defines the content of the tab. If you have
more than one element in your tab, which you most likely will, it is
important that you wrap all the content in a div() tagid - an ID for the tabtabset() has an optional parameter:
menu_class, which we can use to change the appearance of
the tabs.
A nice benefit of shiny.semantic is its class system,
which tends to be simpler and more intuitive than Bootstrap’s. As an
example, we use class = "ui basic segment" to define the
type of segment we would like the content of our tabs to sit in. With
the Semantic classes we can quite quickly experiment with the look of
our apps. Sadly, the class system can’t do everything so you may need to
dabble in some CSS to get things looking exactly as you want it. For
example, it’d be nice to remove the borders surrounding the content in
the tabs.
Another shiny.semantic feature to be aware of is
semantic_DT(), which generates a DT table in the Semantic
style. It sits in the server inside renderDataTable() and
takes a data frame as an argument. (Note that in the UI, we consequently
use semantic_DTOutput() instead of
DT::dataTableOutput())
Now that we’ve got the layout side of styling our Shiny apps sorted, we’ll explore the theming tools available in the three frameworks.
bslibTheming in bslib is extremely straightforward to do. You
can create a dashboard with quite a bespoke, professional feel very
quickly.
bs_theme()The bs_theme() function does most of the heavy lifting
for you. All we need to do is provide values for the properties we want
to change, e.g. background colour, bg, and set the value of
page_navbar()’s theme argument as bs_theme(),
i.e. theme = bs_theme().
thematic_shiny()The one downside to bs_theme() is that plotOutputs are
unaffected so they can look out of place. Fortunately, the
thematic package implements another easy-to-use solution in
thematic_shiny(), which grabs the CSS styles surrounding
plots and applies them to the plots (Note: it works for
ggplot2, base and lattice
visualisations). We call the function outside the UI, and in a similar
way to bs_theme(), we just provide values for the
properties we want to change. And, to make our lives even easier, we can
set some of the parameters to "auto" so that the plot’s
theme inherits the theming we specify in bs_theme().
With a few lines of code, our bslib app is almost
complete. It’s a small detail but we have a rounded border surrounding
the content in the Project FeederWatch tab but not the other two. We can
add one with a bit of CSS using nav_panel()’s class
argument, as in line 144. You can compare how this looks by running the
app and checking it against the images above.
shinydashboardTheming with shinydashboard is limited. We really only
have one option to play with and that’s the skin parameter
of dashboardPage(). It defaults to blue (as we saw above)
and only takes a handful of named colours without CSS, it isn’t really
possible to create something bespoke or aesthetically pleasing.
shiny.semanticAgain, theming here is limited. It’s very much reliant on using
classes, for example, in line 113, where we give the action button the
"ui primary button" class so it takes on the primary
colour. Semantic has a default set of named colours, which you can
change using a CSS file in which you define those named colours.
Although we’re not exploring how CSS can be used to style Shiny apps
here, the overall appearance of a shiny.semantic app, even
without colour, is nice and clean.
And, just like that, we’ve created a Shiny app and explored how it can be themed under three different frameworks! It can be hard to learn Shiny without having some templates that show how everything fits together so, hopefully, this guide has helped you understand how we can style apps and, particularly, if you’re still fairly new to Shiny, shown you it’s not necessarily as tricky or intimidating as it might seem.